//
//  BMSnomPhone.m
//  snomPhone
//
//  Created by Daniel Hazelbaker on 11/2/08.
//  Copyright 2008 Blue Box Moon. All rights reserved.
//

#import "BMSnomPhone.h"


static BMSnomPhone *_sharedPhone = nil;

@implementation BMSnomPhone


//
// Retrieve the current snom Phone object.
//
+ (BMSnomPhone *)sharedPhone
{
    return _sharedPhone;
}


//
// Strip the phone number so only digits are left.
//
+ (NSString *)strippedPhoneNumber:(NSString *)phone
{
    NSMutableString *phoneString = [NSMutableString string];
    NSCharacterSet  *numericCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];
    int		    i;


    for (i = 0; i < [phone length]; i++) {
	if ([numericCharacterSet characterIsMember:[phone characterAtIndex:i]])
	    [phoneString appendFormat:@"%c", [phone characterAtIndex:i]];
    }

    return phoneString;
}


//
// Format a phone number to be a standard number.
//
+ (NSString *)formatPhoneNumber:(NSString *)number
{
    //
    // Strip the phone number and remove the leading 1 if there is one.
    //
    number = [self strippedPhoneNumber:number];
    if ([number characterAtIndex:0] == '1')
	number = [number substringFromIndex:1];

    if ([number length] == 7)
	return [NSString stringWithFormat:@"%@-%@",
		[number substringWithRange:NSMakeRange(0, 3)],
		[number substringWithRange:NSMakeRange(3, 4)]];
    else if ([number length] == 10)
	return [NSString stringWithFormat:@"(%@) %@-%@",
		[number substringWithRange:NSMakeRange(0, 3)],
		[number substringWithRange:NSMakeRange(3, 3)],
		[number substringWithRange:NSMakeRange(6, 4)]];
    else if ([number length] == 13 || [number length] == 14)
	return [NSString stringWithFormat:@"(%@) %@-%@ x%@",
		[number substringWithRange:NSMakeRange(0, 3)],
		[number substringWithRange:NSMakeRange(3, 3)],
		[number substringWithRange:NSMakeRange(6, 4)],
		[number substringFromIndex:10]];
    
    return number;
}



//
// Initialize a new snome phone object with the given address.
// If the address is nil then no object is created and nil is
// returned. Otherwise it can be either a numeric IP address or
// a DNS name to lookup.
//
- (id)initWithAddress:(NSString *)address
{
    return [self initWithAddress:address password:nil];
}


//
// Initialize a new snom phone object with the given address.
// If the address is nil then no object is created and nil is
// returned. Otherwise it can be either a numeric IP address or
// a DNS name to lookup. If password is not empty then the password
// is used to communicate with the snom phone.
//
- (id)initWithAddress:(NSString *)address password:(NSString *)password
{
    if ([self init] == nil)
	return nil;

    //
    // Check if we have a (somewhat) valid address.
    //
    if ([address length] == 0) {
	[self release];
	[NSException raise:NSInvalidArgumentException format:@"Address cannot be an empty string."];

	return nil;
    }

    //
    // Store the information for later retrieval.
    //
    [self setValue:address forKey:@"phoneAddress"];
    [self setValue:password forKey:@"phonePassword"];

    //
    // Do an initial check to see if we can talk to the phone.
    //
    if ([[self requestPhoneURL:@"/index.htm" postData:nil] length] == 0) {
	[self release];
	[NSException raise:NSGenericException format:@"Cannot contact phone web service."];

	return nil;
    }

    //
    // Retrieve the initial settings from the phone.
    //
    [self loadCallHistory];
    [self loadPhoneSettings];

    _sharedPhone = self;
    
    return self;
}


//
// Release all memory and objects used by this snom phone object.
//
- (void)dealloc
{
    [self setValue:nil forKey:@"phonePassword"];
    [self setValue:nil forKey:@"phoneAddress"];
    [self setValue:nil forKey:@"callHistory"];
    [self setValue:nil forKey:@"phoneSettings"];

    [super dealloc];
}


//
// Build the phone URL required for the given urlPath. This path should be
// absolute (i.e. "/index.html").
//
- (NSURL *)buildPhoneURL:(NSString *)urlPath
{
    //
    // Build up the URL and the request.
    //
    if ([phonePassword length] > 0)
	return [NSURL URLWithString:[NSString stringWithFormat:@"http://admin:%@@%@%@", phonePassword, phoneAddress, urlPath]];
    else
	return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@%@", phoneAddress, urlPath]];
}


//
// Request a URL from the phone. This path should be absolute (i.e. "/index.html").
// The result data is returned as a string object. If postData is not empty then
// the request is sent as an HTTP POST method with the given postData. If an error
// occured then nil is returned.
//
- (NSString *)requestPhoneURL:(NSString *)urlPath postData:(NSString *)postData
{
    NSMutableURLRequest	*request;
    NSHTTPURLResponse	*response;
    NSString		*string;
    NSError		*error = nil;
    NSData		*data;
    NSURL		*url;


    //
    // Build up the URL and the request.
    //
    url = [self buildPhoneURL:urlPath];
    request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10];

    //
    // Setup the postData if applicable.
    //
    if ([postData length] > 0) {
	[request setHTTPMethod:@"POST"];
	[request setHTTPBody:[postData dataUsingEncoding:NSUTF8StringEncoding]];
    }

    //
    // Submit the URL request.
    //
    data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
    if ([response statusCode] == 401)
	[NSException raise:NSGenericException format:@"401 Unauthorized"];

    //
    // Check for valid data and conver to a string.
    //
    if (data == nil)
	return nil;
    string = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];

    //
    // Check if the string is saying we are unauthorized.
    //
    if ([string length] < 2048 && ([string rangeOfString:@"Unauthorized request"].location != NSNotFound ||
	[string rangeOfString:@"<B>Login failed!</B>"].location != NSNotFound))
	[NSException raise:NSGenericException format:@"401 Unauthorized"];

    return string;
}


//
// Load the call history from the phone. We don't do any major error checking
// but if the XML doesn't line up then we just give up.
//
- (void)loadCallHistory
{
    NSMutableDictionary	*history = [NSMutableDictionary dictionary];
    NSMutableArray	*calls;
    NSXMLDocument	*document;
    NSDictionary	*call;
    NSXMLNode		*parent;
    NSArray		*nodes;
    NSError		*error = nil;
    int			i;


    //
    // Retrieve the web page from the Snom phone.
    //
    document = [[[NSXMLDocument alloc] initWithXMLString:[self requestPhoneURL:@"/index.htm" postData:nil] options:(NSXMLDocumentTidyHTML|NSXMLDocumentTidyXML) error:&error] autorelease];
    if (document == nil)
	return;

    //
    // Process dialed calls.
    //
    nodes = [document objectsForXQuery:@".//a[@name=\"list_dialed\"]" error:nil];
    calls = [NSMutableArray array];
    if ([nodes count] > 0) {
	parent = [[[[nodes objectAtIndex:0] parent] parent] parent];
	for (i = 3; i < [parent childCount] && [calls count] < 10; i++) {
	    call = [self processCallHistoryNode:[parent childAtIndex:i] isMissed:NO];
	    if (call != nil)
		[calls addObject:call];
	}
    }
    [history setValue:calls forKey:@"dialedCalls"];

    //
    // Process missed calls.
    //
    nodes = [document objectsForXQuery:@".//a[@name=\"list_missed\"]" error:nil];
    calls = [NSMutableArray array];
    if ([nodes count] > 0) {
	parent = [[[[nodes objectAtIndex:0] parent] parent] parent];
	for (i = 3; i < [parent childCount] && [calls count] < 10; i++) {
	    [calls addObject:[self processCallHistoryNode:[parent childAtIndex:i] isMissed:YES]];
	}
    }
    [history setValue:calls forKey:@"missedCalls"];

    //
    // Process received calls.
    //
    nodes = [document objectsForXQuery:@".//a[@name=\"list_received\"]" error:nil];
    calls = [NSMutableArray array];
    if ([nodes count] > 0) {
	parent = [[[[nodes objectAtIndex:0] parent] parent] parent];
	for (i = 3; i < [parent childCount] && [calls count] < 10; i++) {
	    [calls addObject:[self processCallHistoryNode:[parent childAtIndex:i] isMissed:NO]];
	}
    }
    [history setValue:calls forKey:@"receivedCalls"];

    [self setValue:history forKey:@"callHistory"];
}


//
// Process a call history node from the XML data. If the missed flag is
// turned on then the node is a missed call record, otherwise it is a
// dialed or received call record.
//
- (NSDictionary *)processCallHistoryNode:(NSXMLNode *)node isMissed:(BOOL)missed
{
    NSCalendarDate	*callDate, *now;
    NSCharacterSet	*whitespace = [NSCharacterSet whitespaceCharacterSet];
    NSString		*date = nil, *time = nil, *duration = nil, *number = nil, *name = nil,
			*relativeDuration = nil, *relativeShortDuration = nil, *relativeDate = nil,
			*relativeDateTime = nil;
    NSArray		*components;
    int			hours, minutes, seconds;


    //
    // Get the values from the node and trim off anything we don't want.
    //
    if (missed == NO) {
	date = [[[node childAtIndex:0] stringValue] stringByTrimmingCharactersInSet:whitespace];
	time = [[[node childAtIndex:1] stringValue] stringByTrimmingCharactersInSet:whitespace];
	duration = [[[node childAtIndex:2] stringValue] stringByTrimmingCharactersInSet:whitespace];
	number = [[[node childAtIndex:5] stringValue] stringByTrimmingCharactersInSet:whitespace];
    }
    else {
	date = [[[node childAtIndex:0] stringValue] stringByTrimmingCharactersInSet:whitespace];
	time = [[[node childAtIndex:1] stringValue] stringByTrimmingCharactersInSet:whitespace];
	number = [[[node childAtIndex:4] stringValue] stringByTrimmingCharactersInSet:whitespace];
    }

    //
    // Split out the number and name.
    //
    components = [number componentsSeparatedByString:@" <"];
    if ([components count] == 1) {
	components = [number componentsSeparatedByString:@":"];
	if ([components count] == 1)
	    return nil;
	number = [[[components objectAtIndex:1] componentsSeparatedByString:@"@"] objectAtIndex:0];
	name = number;
    }
    else {
	name = [components objectAtIndex:0];
	number = [[[[[components objectAtIndex:1] componentsSeparatedByString:@":"] objectAtIndex:1] componentsSeparatedByString:@"@"] objectAtIndex:0];
    }

    //
    // Parse the date.
    //
    callDate = [NSCalendarDate dateWithString:[NSString stringWithFormat:@"%@ %@", date, time] calendarFormat:@"%m/%e/%Y %I:%M%p"];
    now = [NSCalendarDate date];
    if ([now dayOfCommonEra] == [callDate dayOfCommonEra]) {
	relativeDate = @"Today";
	relativeDateTime = time;
    }
    else if (([now dayOfCommonEra] - 1) == [callDate dayOfCommonEra]) {
	relativeDate = @"Yesterday";
	relativeDateTime = [NSString stringWithFormat:@"Yesterday at %@", time];
    }
    else {
	relativeDate = date;
	relativeDateTime = [NSString stringWithFormat:@"%@ on %@", time, date];
    }

    //
    // Parse the time, convert the "AM/PM" designation to just "a/p".
    //
    if ([[time substringWithRange:NSMakeRange([time length] - 2, 2)] isEqual:@"AM"])
	time = [NSString stringWithFormat:@"%@a", [time substringToIndex:([time length] - 2)]];
    else
	time = [NSString stringWithFormat:@"%@p", [time substringToIndex:([time length] - 2)]];
    
    //
    // Parse the duration.
    //
    components = [duration componentsSeparatedByString:@":"];
    if ([components count] == 3) {
	hours = [[components objectAtIndex:0] intValue];
	minutes = [[components objectAtIndex:1] intValue];
	seconds = [[components objectAtIndex:2] intValue];
    }
    else {
	hours = 0;
	minutes = [[components objectAtIndex:0] intValue];
	seconds = [[components objectAtIndex:1] intValue];
    }
    if (hours > 0) {
	duration = [NSString stringWithFormat:@"%d:%02d:%02d", hours, minutes, seconds];
	relativeDuration = [NSString stringWithFormat:@"%d hour%s, %d minute%s and %d second%s",
			hours, (hours > 1 ? "s" : ""), minutes, (minutes > 1 ? "s" : ""), seconds, (seconds > 1 ? "s" : "")];
	relativeShortDuration = [NSString stringWithFormat:@"%d hour%s, %d min%s and %d sec%s",
			hours, (hours > 1 ? "s" : ""), minutes, (minutes > 1 ? "s" : ""), seconds, (seconds > 1 ? "s" : "")];
    }
    else if (minutes > 0) {
	duration = [NSString stringWithFormat:@"%d:%02d", minutes, seconds];
	relativeDuration = [NSString stringWithFormat:@"%d minute%s and %d second%s",
			minutes, (minutes > 1 ? "s" : ""), seconds, (seconds > 1 ? "s" : "")];
	relativeShortDuration = [NSString stringWithFormat:@"%d min%s and %d sec%s",
			minutes, (minutes > 1 ? "s" : ""), seconds, (seconds > 1 ? "s" : "")];
    }
    else {
	duration = [NSString stringWithFormat:@"0:%02d", seconds];
	relativeDuration = [NSString stringWithFormat:@"%d second%s",
			seconds, (seconds > 1 ? "s" : "")];
	relativeShortDuration = [NSString stringWithFormat:@"%d sec%s",
			seconds, (seconds > 1 ? "s" : "")];
    }

    //
    // Build the call dictionary.
    //
    if (missed == NO) {
	return [NSDictionary dictionaryWithObjectsAndKeys:
			name, @"name",
			number, @"number",
			date, @"date",
			time, @"time",
			relativeDate, @"relativeDate",
			relativeDateTime, @"relativeDateTime",
			callDate, @"callDate",
			[NSString stringWithFormat:@"%@ %@", date, time], @"dateTime",
			duration, @"duration",
			relativeDuration, @"relativeDuration",
			relativeShortDuration, @"relativeShortDuration",
			nil];
    }
    else {
	return [NSDictionary dictionaryWithObjectsAndKeys:
			name, @"name",
			number, @"number",
			date, @"date",
			time, @"time",
			relativeDate, @"relativeDate",
			relativeDateTime, @"relativeDateTime",
			callDate, @"callDate",
			[NSString stringWithFormat:@"%@ %@", date, time], @"dateTime",
			nil];
    }
}


//
// Load the phone settings from the phone. We don't really do anything except
// store all the settings in a dictionary that can be retrieved at a later
// time.
//
- (void)loadPhoneSettings
{
    NSMutableDictionary	*settings;
    NSCharacterSet	*codeCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"&!"];
    NSString		*settingsString, *key, *value;
    NSArray		*array, *components;
    int			i;


    //
    // Retrieve the web page from the Snom phone.
    //
    settings = [NSMutableDictionary dictionary];
    settingsString = [self requestPhoneURL:@"/settings.cfg" postData:nil];
    array = [settingsString componentsSeparatedByString:@"\r\n"];
    for (i = 0; i < [array count]; i++) {
	//
	// Split this setting out to its key/value pair.
	//
	components = [[array objectAtIndex:i] componentsSeparatedByString:@": "];
	if ([components count] != 2)
	    continue;
	key = [components objectAtIndex:0];
	value = [components objectAtIndex:1];

	//
	// Strip off any special characters from the key.
	//
	key = [key stringByTrimmingCharactersInSet:codeCharacterSet];

	[settings setObject:value forKey:key];
    }

    [self setValue:settings forKey:@"phoneSettings"];
}


//
// Set a phone setting. If you are setting more than one key/value pair then
// you should use the setPhoneSettingsFromDictionary: method to process all
// settings at once.
//
- (void)setPhoneSetting:(id)value forKey:(NSString *)key
{
    if ([self requestPhoneURL:@"/prefs.htm" postData:[NSString stringWithFormat:@"%@=%@&Settings=Save", key, value]] != nil)
	[phoneSettings setObject:value forKey:key];
}


//
// Set a group of phone settings at once. 
- (void)setPhoneSettingsFromDictionary:(NSDictionary *)settings
{
    NSMutableString	*post;
    NSArray		*keys;
    int			i;


    //
    // Walk all the settings and put them in the post data.
    //
    keys = [settings allKeys];
    post = [NSMutableString string];
    for (i = 0; i < [keys count]; i++) {
	if (i > 0)
	    [post appendString:@"&"];
	[post appendFormat:@"%@=%@", [keys objectAtIndex:i], [settings objectForKey:[keys objectAtIndex:i]]];
    }

    //
    // Append the final "save" command.
    //
    [post appendFormat:@"&Settings=Save"];

    if ([self requestPhoneURL:@"/prefs.htm" postData:post] != nil)
	[phoneSettings takeValuesFromDictionary:settings];
}


//
// Retrieve the cached call history from the phone.
//
- (NSDictionary *)callHistory
{
    return callHistory;
}


//
// Retrieve the cached phone settings from the phone.
//
- (NSDictionary *)phoneSettings
{
    return phoneSettings;
}


//
// Dial the given phone number on the phone.
//
- (void)dialPhoneNumber:(NSString *)phoneNumber
{
    NSString	*urlString;


    urlString = [NSString stringWithFormat:@"/index.htm?number=%@", phoneNumber];
    [self requestPhoneURL:urlString postData:nil];
}


@end
